Skip to main content

1.6 JavaScript Basics: Collision Detection in Super Mario Bros

image

Welcome back, young game developers! In our previous lessons, we've learned how to bring Mario to life with movement and animation. Now, it's time to take our game to the next level by introducing collision detection. This crucial feature will allow Mario to interact with other elements in the game world, making our Super Mario Bros. clone even more exciting and challenging!

What is Collision Detection?

Imagine you're playing tag with your friends in the playground. When you touch someone, you've "collided" with them. In video games, collision detection works similarly. It's how we determine when game objects touch or overlap each other. This is essential for many game mechanics, such as:

  • Mario jumping on enemies
  • Collecting coins or power-ups
  • Preventing Mario from walking through walls or falling through the ground

Without collision detection, our game objects would just pass through each other like ghosts! That wouldn't be very fun, would it?

Common Methods for Collision Detection in Web Games

There are several ways to implement collision detection in web games. Let's explore some of the most common methods:

  1. Bounding Box Collision: This is the simplest and most widely used method. We treat each object as a rectangle and check if these rectangles overlap.

  2. Circle Collision: For round objects, we can use circle collision detection. This involves checking the distance between the centers of two circles.

  3. Pixel-Perfect Collision: This is the most accurate but also the most computationally expensive method. It checks collision at the pixel level.

  4. Swept AABB: This method predicts collisions by considering the movement of objects between frames.

For our Super Mario Bros. game, we'll use the Bounding Box Collision method as it's simple to implement and works well for most 2D games.

Understanding the Collision Detection Code

Let's take a closer look at the collision detection code in our game:

function checkCollision(rect1, rect2) {
return (
rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y
);
}

This function takes two rectangles (rect1 and rect2) as input and returns true if they overlap, and false if they don't. Here's how it works:

  1. rect1.x < rect2.x + rect2.width: Checks if the left edge of rect1 is to the left of the right edge of rect2.
  2. rect1.x + rect1.width > rect2.x: Checks if the right edge of rect1 is to the right of the left edge of rect2.
  3. rect1.y < rect2.y + rect2.height: Checks if the top edge of rect1 is above the bottom edge of rect2.
  4. rect1.y + rect1.height > rect2.y: Checks if the bottom edge of rect1 is below the top edge of rect2.

If all these conditions are true, it means the rectangles are overlapping.

Implementing Collision Detection in Our Game

Now that we understand how collision detection works, let's see how we use it in our game loop:

function gameLoop() {
if (isGameOver) return;

const marioRect = mario.getBoundingClientRect();
const goombaRect = goomba.getBoundingClientRect();

if (checkCollision(marioRect, goombaRect)) {
if (marioRect.bottom < goombaRect.top + goombaRect.height / 2) {
// Mario landed on top of Goomba
defeatGoomba();
} else {
// Mario collided with Goomba from the side
gameOver();
}
}

requestAnimationFrame(gameLoop);
}

Here's what's happening:

  1. We get the bounding rectangles of Mario and the Goomba using getBoundingClientRect().
  2. We check if these rectangles are colliding using our checkCollision() function.
  3. If there's a collision, we check if Mario is on top of the Goomba (for a successful stomp) or if he hit the Goomba from the side (game over).
  4. Depending on the type of collision, we either defeat the Goomba or end the game.

Making Goomba Move After Mario

Let's make the Goomba start moving only after Mario has moved or jumped. This will make the game more interesting and give the player a moment to prepare.

let hasMariolMoved = false;

function checkMarioMovement() {
if (!hasMariolMoved && (isMovingRight || isMovingLeft || isJumping)) {
hasMariolMoved = true;
moveGoomba();
}
requestAnimationFrame(checkMarioMovement);
}

checkMarioMovement();

Making the Game More Interactive

Now that we understand collision detection, let's make our game more exciting!

When Mario defeats a Goomba, we want to:

  1. Play a sound effect
  2. Make the Goomba fall off the screen
  3. Spawn a new Goomba

Here's how we do it:

function defeatGoomba() {
playSound(
"https://www.weblab.fun/audio/tutorials_frontend_development/mario_game/kick.mp3"
);

goomba.style.bottom = "110px";
goomba.style.animation = "fall 0.5s linear";
goomba.style.transform = "rotate(180deg)";

setTimeout(() => {
goomba.remove();
spawnNewGoomba();
}, 500);
}

function spawnNewGoomba() {
const newGoomba = document.createElement("div");
newGoomba.className = "goomba";
newGoomba.style.left = "800px";
document.querySelector(".game-container").appendChild(newGoomba);
goomba = newGoomba;
goombaPosition = window.innerWidth;
goombaSpeed *= 1.1; // Increase speed by 10%
}

This code makes the Goomba fall off the screen, plays a sound, and then creates a new, faster Goomba after half a second.

Styling for Goomba fall

Let's add the CSS transform for goomba fall animation:

@keyframes fall {
0% {
transform: translateY(0) rotate(0deg);
}
100% {
transform: translateY(200px) rotate(180deg);
}
}

This animation will make Goomba fall and rotate when defeated.

Game Over Scenario

When Mario collides with a Goomba from the side, it's game over:

function gameOver() {
isGameOver = true;
backgroundMusic.pause();
playSound(
"https://www.weblab.fun/audio/tutorials_frontend_development/mario_game/game_over.mp3"
);

const gameOverText = document.createElement("div");
gameOverText.textContent = "GAME OVER";
gameOverText.style.position = "absolute";
gameOverText.style.top = "50%";
gameOverText.style.left = "50%";
gameOverText.style.transform = "translate(-50%, -50%)";
gameOverText.style.fontSize = "64px";
gameOverText.style.color = "white";
document.querySelector(".game-container").appendChild(gameOverText);

const restartButton = document.createElement("div");
restartButton.textContent = "Restart";
restartButton.style.position = "absolute";
restartButton.style.top = "60%";
restartButton.style.left = "50%";
restartButton.style.fontSize = "24px";
restartButton.style.color = "white";
restartButton.style.cursor = "pointer";
restartButton.style.transform = "translate(-50%, -50%)";
restartButton.addEventListener("click", restartGame);
document.querySelector(".game-container").appendChild(restartButton);
}

This function stops the game, plays a game over sound, and shows a "GAME OVER" message with a restart button.

Let's See It in Action!

Here's a complete, runnable example of our Super Mario Bros. game with collision detection:

http://localhost:3000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Super Mario Bros.</title>
<link
href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap"
rel="stylesheet"
/>
<style>
body,
html {
margin: 0;
padding: 0;
height: 100%;
font-family: "Press Start 2P", cursive;
}
.game-container {
width: 100%;
height: 100%;
background-color: #5c94fc;
position: relative;
overflow: hidden;
box-shadow: inset 0 0 100px rgba(0, 0, 0, 0.3);
}
.stats {
display: flex;
justify-content: space-around;
padding: 8px;
color: white;
font-size: 22px;
font-weight: bold;
text-align: center;
}
.title {
position: absolute;
top: 200px;
left: 100px;
background-color: #e86a17;
color: #ffd8d8;
padding: 16px;
font-size: 52px;
font-weight: bold;
text-align: left;
line-height: 70px;
border-radius: 8px;
text-shadow: 8px 8px #000;
box-shadow: 4px 4px #000;
}
.copyright {
position: absolute;
top: 382px;
left: 426px;
color: #ffd8d8;
font-size: 20px;
}
.cloud {
position: absolute;
top: 200px;
right: 200px;
width: 140px;
height: 100px;
background-image: url("https://www.weblab.fun/img/tutorials_frontend_development/mario_game/cloud.png");
background-size: contain;
}
.block {
position: absolute;
width: 40px;
height: 40px;
border: 1px solid #000;
text-align: center;
line-height: 40px;
}
.block1,
.block3,
.block5 {
background-image: url("https://www.weblab.fun/img/tutorials_frontend_development/mario_game/block.png");
background-size: cover;
}
.block2,
.block4 {
background-image: url("https://www.weblab.fun/img/tutorials_frontend_development/mario_game/question_block.gif");
background-size: cover;
}
.block1 {
bottom: 300px;
right: 320px;
}
.block2 {
bottom: 300px;
right: 280px;
}
.block3 {
bottom: 300px;
right: 240px;
}
.block4 {
bottom: 300px;
right: 200px;
}
.block5 {
bottom: 300px;
right: 160px;
}
.mario {
position: absolute;
z-index: 1;
bottom: 150px;
left: 50px;
width: 35px;
height: 40px;
background-image: url("https://www.weblab.fun/img/tutorials_frontend_development/mario_game/mario.png");
background-size: cover;
}
.goomba {
position: absolute;
z-index: 1;
bottom: 150px;
right: 0px;
width: 40px;
height: 40px;
background-image: url("https://www.weblab.fun/img/tutorials_frontend_development/mario_game/goomba.gif");
background-size: cover;
}
.hill {
position: absolute;
bottom: 150px;
width: 300px;
height: 150px;
background-image: url("https://www.weblab.fun/img/tutorials_frontend_development/mario_game/hill.png");
background-size: cover;
border-radius: 50% 50% 0 0;
}
.hill1 {
left: 0px;
}
.hill2 {
right: 200px;
width: 200px;
height: 100px;
}
.ground {
position: absolute;
bottom: 0;
width: 100%;
height: 150px;
background-image: url("https://www.weblab.fun/img/tutorials_frontend_development/mario_game/ground.jpg");
background-repeat: repeat-x;
background-size: contain;
}
@keyframes fall {
0% {
transform: translateY(0) rotate(0deg);
}
100% {
transform: translateY(200px) rotate(180deg);
}
}
</style>
</head>
<body>
<div class="game-container">
<div class="stats">
<span>SCORE<br />0</span>
<span>COINS<br />0</span>
<span>WORLD<br />1-1</span>
<span>TIME<br />378</span>
<span>LIVES<br />3</span>
</div>
<div class="title">SUPER<br />MARIO BROS.</div>
<div class="copyright">©1985 NINTENDO</div>
<div class="cloud"></div>
<div class="block block1"></div>
<div class="block block2"></div>
<div class="block block3"></div>
<div class="block block4"></div>
<div class="block block5"></div>
<div class="mario"></div>
<div class="goomba"></div>
<div class="hill hill1"></div>
<div class="hill hill2"></div>
<div class="ground"></div>
</div>
<audio id="backgroundMusic" loop>
<source
src="https://www.weblab.fun/audio/tutorials_frontend_development/mario_game/background_music.mp3"
type="audio/mpeg"
/>
</audio>
<audio id="jumpSound">
<source
src="https://www.weblab.fun/audio/tutorials_frontend_development/mario_game/jump_small.mp3"
type="audio/mpeg"
/>
</audio>
<script>
const mario = document.querySelector(".mario");
const backgroundMusic = document.getElementById("backgroundMusic");
const jumpSound = document.getElementById("jumpSound");
let goomba = document.querySelector(".goomba");
let position = 50;
let isJumping = false;
let isFacingRight = true;
let isMovingRight = false;
let isMovingLeft = false;
let isMusicPlaying = false;
let goombaPosition = window.innerWidth;
let goombaDirection = -1;
let goombaSpeed = 2;
let isGameOver = false;
let hasMariolMoved = false;

document.addEventListener("keydown", (e) => {
if (isGameOver) return;

if (!isMusicPlaying) {
backgroundMusic.play();
isMusicPlaying = true;
}

if (e.key === "ArrowRight") {
isMovingRight = true;
position += 10;
mario.style.left = `${position}px`;
if (!isJumping)
mario.style.backgroundImage =
"url('https://www.weblab.fun/img/tutorials_frontend_development/mario_game/mario_run.gif')";
mario.style.transform = "scaleX(1)";
isFacingRight = true;
} else if (e.key === "ArrowLeft") {
isMovingLeft = true;
position -= 10;
mario.style.left = `${position}px`;
if (!isJumping)
mario.style.backgroundImage =
"url('https://www.weblab.fun/img/tutorials_frontend_development/mario_game/mario_run.gif')";
mario.style.transform = "scaleX(-1)";
isFacingRight = false;
} else if (e.code === "Space" && !isJumping) {
jumpSound.currentTime = 0;
jumpSound.play();
jump();
}
});

document.addEventListener("keyup", (e) => {
if (e.key === "ArrowRight") {
isMovingRight = false;
} else if (e.key === "ArrowLeft") {
isMovingLeft = false;
}

if (!isMovingRight && !isMovingLeft && !isJumping) {
mario.style.backgroundImage =
"url('https://www.weblab.fun/img/tutorials_frontend_development/mario_game/mario.png')";
mario.style.transform = isFacingRight ? "scaleX(1)" : "scaleX(-1)";
}
});

function jump() {
isJumping = true;
let jumpHeight = 0;
mario.style.backgroundImage =
"url('https://www.weblab.fun/img/tutorials_frontend_development/mario_game/mario_jump.png')";
const jumpInterval = setInterval(() => {
if (jumpHeight >= 150) {
clearInterval(jumpInterval);
fall();
} else {
jumpHeight += 5;
mario.style.bottom = `${150 + jumpHeight}px`;

if (isMovingRight) {
position += 5;
mario.style.left = `${position}px`;
} else if (isMovingLeft) {
position -= 5;
mario.style.left = `${position}px`;
}
}
}, 10);
}

function fall() {
let fallHeight = 150;
const fallInterval = setInterval(() => {
if (fallHeight <= 0) {
clearInterval(fallInterval);
mario.style.bottom = "150px";
isJumping = false;
if (isMovingRight || isMovingLeft) {
mario.style.backgroundImage =
"url('https://www.weblab.fun/img/tutorials_frontend_development/mario_game/mario_run.gif')";
} else {
mario.style.backgroundImage =
"url('https://www.weblab.fun/img/tutorials_frontend_development/mario_game/mario.png')";
}
} else {
fallHeight -= 5;
mario.style.bottom = `${150 + fallHeight}px`;

if (isMovingRight) {
position += 5;
mario.style.left = `${position}px`;
} else if (isMovingLeft) {
position -= 5;
mario.style.left = `${position}px`;
}
}
}, 10);
}

function moveGoomba() {
if (isGameOver) return;

goombaPosition += goombaDirection * goombaSpeed;
goomba.style.left = `${goombaPosition}px`;

if (goombaPosition <= 0 || goombaPosition >= window.innerWidth) {
goombaDirection *= -1;
}

requestAnimationFrame(moveGoomba);
}

function checkCollision(rect1, rect2) {
return (
rect1.x < rect2.x + rect2.width &&
rect1.x + rect1.width > rect2.x &&
rect1.y < rect2.y + rect2.height &&
rect1.y + rect1.height > rect2.y
);
}

function gameLoop() {
if (isGameOver) return;

const marioRect = mario.getBoundingClientRect();
const goombaRect = goomba.getBoundingClientRect();

if (checkCollision(marioRect, goombaRect)) {
if (marioRect.bottom < goombaRect.top + goombaRect.height / 2) {
// Mario landed on top of Goomba
defeatGoomba();
} else {
// Mario collided with Goomba from the side
gameOver();
}
}

requestAnimationFrame(gameLoop);
}

function defeatGoomba() {
playSound(
"https://www.weblab.fun/audio/tutorials_frontend_development/mario_game/kick.mp3"
);

goomba.style.bottom = "110px";
goomba.style.animation = "fall 0.5s linear";
goomba.style.transform = "rotate(180deg)";

setTimeout(() => {
goomba.remove();
spawnNewGoomba();
}, 500);
}

function spawnNewGoomba() {
const newGoomba = document.createElement("div");
newGoomba.className = "goomba";
newGoomba.style.left = "800px";
document.querySelector(".game-container").appendChild(newGoomba);
goomba = newGoomba;
goombaPosition = window.innerWidth;
goombaSpeed *= 1.1; // Increase speed by 10%
}

function gameOver() {
isGameOver = true;
backgroundMusic.pause();
playSound(
"https://www.weblab.fun/audio/tutorials_frontend_development/mario_game/game_over.mp3"
);

const gameOverText = document.createElement("div");
gameOverText.textContent = "GAME OVER";
gameOverText.style.position = "absolute";
gameOverText.style.top = "50%";
gameOverText.style.left = "50%";
gameOverText.style.width = "576px";
gameOverText.style.transform = "translate(-50%, -50%)";
gameOverText.style.fontSize = "64px";
gameOverText.style.color = "white";
document.querySelector(".game-container").appendChild(gameOverText);

const restartButton = document.createElement("div");
restartButton.textContent = "Restart";
restartButton.style.position = "absolute";
restartButton.style.top = "60%";
restartButton.style.left = "50%";
restartButton.style.fontSize = "24px";
restartButton.style.color = "white";
restartButton.style.cursor = "pointer";
restartButton.style.transform = "translate(-50%, -50%)";
restartButton.addEventListener("click", restartGame);
document.querySelector(".game-container").appendChild(restartButton);
}

function restartGame() {
location.reload();
}

function playSound(src, loop) {
const sound = new Audio(src);
sound.play();
}

function checkMarioMovement() {
if (!hasMariolMoved && (isMovingRight || isMovingLeft || isJumping)) {
hasMariolMoved = true;
moveGoomba();
}
requestAnimationFrame(checkMarioMovement);
}

checkMarioMovement();
gameLoop();
</script>
</body>
</html>

Conclusion

Congratulations! You've just learned about collision detection in web games. This is a crucial part of game development that makes your games interactive and fun.

In our Super Mario Bros. game, we've implemented:

  1. Collision detection between Mario and Goomba
  2. Different outcomes based on how Mario collides with Goomba
  3. A game over scenario
  4. A way to restart the game

Remember, collision detection is just the beginning. As you continue your journey in game development, you'll discover many more exciting techniques to make your games even more amazing!

Additional Resources

For more advanced collision detection techniques and optimizations, you can check out the MDN Web Docs on 2D collision detection.

Keep coding, keep playing, and most importantly, keep having fun!